// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.api.ads.tools.jaxws;

import com.google.common.annotations.VisibleForTesting;

import org.apache.tools.ant.Task;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

/**
 * This task uses the wsimport tool to generate JAX-WS classes for a product.
 * It combines all of the schema definitions for each namespace from all of the
 * WSDLs of the product and creates binding files for each WSDL.
 *
 * The wsimport tool is currently not capable of correctly generating classes
 * from the fragments of XSD spread across our WSDLs.
 *
 * @author Joseph DiLallo
 */
public class CustomWsimportTask extends Task {

  private String propertiesFilePath;
  private String apiName;
  private String genSrcDir;
  private String apiFramework;
  private ApiProperties properties;

  private DocumentBuilder builder;
  private XPath xPath;
  private Wsimporter wsimporter;
  private BindingsFileFactory bindingsFileFactory;

  private int bindingIndex = 0;

  /**
   * Constructor.
   *
   * Sets up all of the utility classes needed for the task to run.
   */
  public CustomWsimportTask() {
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(true);
    try {
      builder = factory.newDocumentBuilder();
    } catch (ParserConfigurationException e) {
      throw new IllegalStateException("Failed to create a DocumentBuilder.", e);
    }
    xPath = XPathFactory.newInstance().newXPath();
    xPath.setNamespaceContext(new PersonalNamespaceContext());
    wsimporter = new Wsimporter();
    bindingsFileFactory = new BindingsFileFactory();
  }

  /**
   * Testable constructor.
   *
   * @param builder Parses WSDLs into {@code Document} files
   * @param xPath Used to search through {@code Document}s with XPaths
   * @param wsimporter Used to generate classes via the wsimport tool
   * @param bindingsFileFactory Used to create {@code BindingsFile}s
   * @param properties Used to pull information about the API
   */
  @VisibleForTesting
  CustomWsimportTask(DocumentBuilder builder, XPath xPath, Wsimporter wsimporter,
      BindingsFileFactory bindingsFileFactory, ApiProperties properties) {
    this.builder = builder;
    this.xPath = xPath;
    this.wsimporter = wsimporter;
    this.bindingsFileFactory = bindingsFileFactory;
    this.properties = properties;
  }

  public void setPropertiesPath(String propertiesPath) {
    propertiesFilePath = propertiesPath;
  }

  public void setApiName(String apiName) {
    this.apiName = apiName;
  }

  public void setGenSrcDir(String genSrcDir) {
    this.genSrcDir = genSrcDir;
  }

  public void setApiFramework(String apiFramework) {
    this.apiFramework = apiFramework;
  }

  /*
   * @see Task#execute()
   */
  @Override
  public void execute() {
    properties = new ApiProperties(apiName, propertiesFilePath);
    properties.correctPackagePrefix(apiFramework);

    try {
      for (String version : properties.getVersions()) {
        combineWsdlsForVersion(version);
      }
    } catch (IOException e) {
      throw new RuntimeException(e);
    } catch (XPathExpressionException e) {
      throw new RuntimeException(e);
    } catch (SAXException e) {
      throw new RuntimeException(e);
    } catch (TransformerException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Takes a single version of the API and combines all of the WSDLs for it.
   *
   * @param version The version represented as a String
   * @throws MalformedURLException if a WSDL cannot be located
   * @throws IOException if a bindings file cannot be written
   * @throws XPathExpressionException if something goes wrong while searching
   * @throws SAXException if a WSDL cannot be parsed
   * @throws TransformerException if the master WSDL {@code Document} cannot be
   *     transformed to XML
   */
  private void combineWsdlsForVersion(String version)
      throws MalformedURLException, XPathExpressionException, IOException, SAXException,
      TransformerException {

    List<Wsdl> wsdls = new ArrayList<Wsdl>();

    if (Boolean.parseBoolean(properties.hasGroups(version))) {
      for (String group : properties.getGroups(version)) {
        for (String service : properties.getServices(version, group)) {
          wsdls.add(createWsdl(builder, service, xPath, version, group));
        }
      }
    } else {
      for (String service : properties.getServices(version)) {
        wsdls.add(createWsdl(builder, service, xPath, version, null));
      }
    }
    combineWsdls(wsdls);
  }

  /**
   * Creates a {@code Wsdl}.
   *
   * @param builder The builder to use for parsing WSDL documents
   * @param service The service this WSDL defines
   * @param xPath The {@code XPath} object to use to search this WSDL
   * @param version The product version this WSDL represents
   * @param group The group this WSDL belongs to
   * @return A new {@code Wsdl}
   * @throws MalformedURLException if the WSDL document location is malformed
   * @throws IOException if the WSDL document cannot be opened or read
   * @throws SAXException if the WSDL document itself cannot be parsed
   */
  private Wsdl createWsdl(DocumentBuilder builder, String service, XPath xPath, String version,
      String group) throws MalformedURLException, IOException, SAXException {
    return new Wsdl.Builder()
        .withDocumentBuilder(builder)
        .withUrl(generateWsdlUrl(version, service, group))
        .withXPath(xPath)
        .withPackagePrefix(properties.getPackagePrefix())
        .withVersion(version)
        .withGroup(group)
        .build();
  }

  /**
   * Generates the URL of a WSDL service. Only guaranteed to work for Google's
   * Ads APIs.
   *
   * @param version The API version
   * @param service The service whose WSDL URL is needed
   * @param group The group this WSDL belongs to. May be {@code null} if this
   *     API does not use groups
   * @return The URL of the WSDL described
   */
  @VisibleForTesting
  String generateWsdlUrl(String version, String service, String group) {
    return properties.getServerUrl() + "/" + (group != null ? group + "/" : "")
        + version + "/" + service + properties.getWsdlPostfix();
  }

  /**
   * Combines a set of WSDLs. Intended to be used on the complete set of WSDLs
   * from one product for one version, though this is not enforced.
   *
   * @param wsdls All of the WSDLs to be combined
   * @throws MalformedURLException if a WSDL cannot be located
   * @throws IOException if a bindings file cannot be written
   * @throws XPathExpressionException if something goes wrong while searching
   * @throws SAXException if a WSDL cannot be parsed
   * @throws TransformerException if the master WSDL {@code Document} cannot be
   *     transformed to XML
   */
  @VisibleForTesting
  void combineWsdls(List<Wsdl> wsdls)
      throws MalformedURLException, IOException, XPathExpressionException, SAXException,
      TransformerException {
    if (wsdls.size() == 0) {
      throw new IllegalArgumentException("No WSDLs given to parse!");
    }

    Wsdl masterWsdl = wsdls.remove(0);

    for (Wsdl wsdl : wsdls) {
      mergeWsdls(masterWsdl, wsdl);
    }

    outputMasterWsdl(masterWsdl);
  }

  /**
   * Merges two WSDLs together.
   *
   * @param masterWsdl The WSDL into which the other WSDLs will be merged
   * @param wsdl The next WSDL to be merged into the master
   * @throws IOException If a bindings file cannot be created for the non-master
   *     WSDL
   * @throws XPathExpressionException if something goes wrong while searching
   * @throws SAXException if the non-master WSDL cannot be parsed
   */
  private void mergeWsdls(Wsdl masterWsdl, Wsdl wsdl)
      throws IOException, XPathExpressionException, SAXException {
    String bindingFile = getNextBindingFileName();
    BindingsFile bindingFileWriter = bindingsFileFactory.getBindingsFile(bindingFile);
    bindingFileWriter.writeHeader();
    wsdl.writeOperationElementBindings(bindingFileWriter);
    wsdl.writeNonSchemaBinding(bindingFileWriter);
    wsdl.writeSchemaBindings(bindingFileWriter);
    bindingFileWriter.close();

    masterWsdl.mergeInWsdl(wsdl);

    wsimporter.callWsimportOnNonMaster(genSrcDir, bindingFile, wsdl.getUrl());
  }

  /**
   * Writes the master WSDL and a JAXWS bindings file for it to the file system.
   *
   * @param masterWsdl The franken-WSDL created by merging all other WSDLs
   * @throws TransformerException if the {@code Document} cannot be transformed
   *     to XML
   * @throws IOException if a bindings file or master WSDL cannot be written
   * @throws XPathExpressionException if something goes wrong while searching
   */
  private void outputMasterWsdl(Wsdl masterWsdl)
      throws TransformerException, IOException, XPathExpressionException {
    String masterWsdlFileName = genSrcDir + "wsdljoin" + masterWsdl.getVersion() + ".txt";
    masterWsdl.outputDocument(masterWsdlFileName);

    String bindingFilename = getNextBindingFileName();
    BindingsFile masterBindingsFile = bindingsFileFactory.getBindingsFile(bindingFilename);
    masterBindingsFile.writeHeader();
    masterWsdl.writeSchemaBindings(masterBindingsFile);
    masterWsdl.writeNonSchemaBinding(masterBindingsFile);
    masterBindingsFile.close();

    wsimporter.callWsimportOnMaster(genSrcDir, bindingFilename, masterWsdl.getUrl(),
        masterWsdlFileName);
  }

  /**
   * Returns a fully-qualified file path to use for the next bindings file.
   */
  private String getNextBindingFileName() {
    return genSrcDir + "binding" + bindingIndex++ + ".xml";
  }
}
